<?php

class Db_Peer extends Zend_Db_Table_Abstract
{

	const CACHE_TIME_LIMIT = 3600;
	
    /**
     * Classname for row
     *
     * @var string
     */
	protected $_rowClass = 'Db_Row';

	/**
	 * @var Db_Peer
	 */
	private static $_instance;

	/**
	 * @var array
	 */
	private $_cache;

	/**
	 * @return Db_Peer
	 */
	public static function getInstance()
	{
		if (!self::$_instance) {
			self::$_instance = new self();
		}
		return self::$_instance;
	}

	/**
	 * Avoid bug with join.
	 * @link http://zendframework.ru/forum/index.php?topic=894.0
	 *
     * Returns an instance of a Db_Table_Select object.
     *
     * @param string|array $columns
     * @param integer $addFrom OPTIONAL. Default is true.
     * @return Db_Select
     */
    public function select($columns = Db_Select::SQL_WILDCARD)
    {
        $select = new Db_Select($this);
        //$select->__toString();
        //$select->from($this->_name, $columns, $this->_schema); //optimize columns in query
        $select->from($this->_name, Db_Select::SQL_WILDCARD, $this->_schema); //optimize columns in query
        return $select;
    }

    /**
     * Construct must be public for extends work.
     * But we can be sure about object is signleton all the same.
     *
     * @return Db_Peer
     */
    public function __construct($options = array())
    {
    	parent::__construct($options);
    }

    /**
     * Fetch row by column name and value.
     *
     * @param string $column
     * @param string $value
     * @return Db_Row
     */
    public function fetchBy($column, $value)
    {
    	$select = $this->select();
    	$select->where($column . ' = ?', $value);
    	$row = $this->fetchRow($select);
    	return $row;
    }

    /**
     * Fetch by key column.
     * Use Db_Select.
     *
     * @return Db_Row
     */
    public function fetchByKey($key)
    {
    	return $this->fetchBy('`key`', $key);
    }
    
    /**
     * Fetch by id column.
     * Use Db_Select.
     *
     * @return Db_Row
     */
    public function fetchById($id)
    {
    	return $this->fetchBy('`id`', $id);
    }

    /**
     * Return class name by table name.
     *
     * @param string $table
     * @return string
     */
    public static function tableNameToClassName($table)
    {
    	$table = str_replace('_', ' ', $table);
    	$table = ucwords($table);
    	$table = str_replace(' ', '', $table);
    	$table .= 'Peer';
    	return $table;
    }

    /**
     * Get string table name.
     *
     * @return string
     */
    public function getName()
    {
    	return $this->_name;
    }


    /**
     * Return count rows in the table.
     *
     * @param Db_Select $where Custom criteria for count rows.
     * @return integer
     */
    public function fetchCount($where = null)
    {
    	if ($where && !$where instanceof Db_Select) {
    		throw new Exception('Where must by a valid Db_Select object');
    	}

    	if (!$where) {
    		$where = $this->select();
    	}

    	$wherePart = $where->getPart(Db_Select::WHERE);

    	$where->reset();
    	$where->setPart(Db_Select::WHERE, $wherePart);
    	$where->from($this, array('count' => 'count(*)'), $this->_schema);

    	$result = $this->fetchAll($where);
    	return $result[0]['count'];
    }

    /**
     * Cache wrapped of parent::find()
     *
     * Fetches rows by primary key.  The argument specifies one or more primary
     * key value(s).  To find multiple rows by primary key, the argument must
     * be an array.
     *
     * This method accepts a variable number of arguments.  If the table has a
     * multi-column primary key, the number of arguments must be the same as
     * the number of columns in the primary key.  To find multiple rows in a
     * table with a multi-column primary key, each argument must be an array
     * with the same number of elements.
     *
     * The find() method always returns a Rowset object, even if only one row
     * was found.
     *
     * @param  mixed $key The value(s) of the primary keys.
     * @return Zend_Db_Table_Rowset_Abstract Row(s) matching the criteria.
     * @throws Zend_Db_Table_Exception
     */
    public function find($key = null) {
        if (!$key) {
    		return new Zend_Db_Table_Rowset(array());
    	}

    	if (!is_array($key)) {
    		$key = array($key);
    	}

		$cached = array();
		$notCached = array();

		// Looking if there's already rows in cache
		foreach ($key as $pk) {
			if ($this->getCache($pk)) {
				$cached[] = $this->getCache($pk)->toArray();
			} else {
				$notCached[] = $pk;
			}
		}


		// Query for not cached elements
		$notCachedRowset = array();
		if (count($notCached)) {
			$notCachedRowset = parent::find($notCached);
			// push them to cache
			foreach ($notCachedRowset as $row) {
				$this->setCache($row);
				$cached[] = $row->toArray();
			}

		}

		// Configuration for creating Rowset
		$config = array(
            'table'    => $this,
            'data'     => $cached,
            'rowClass' => $this->getRowClass(),
            'readOnly' => false,
            'stored'   => true
        );

        $rowsetClass = $this->getRowsetClass();
		$rowset = new $rowsetClass($config);
        return $rowset;
	}

	/**
	 * Get cached row by id
	 *
	 * @param integer $key Row id from this table
	 * @return Db_Row
	 */
	public function getCache($key, $refColumn = '')
	{
		if (isset($this->$refColumn) && array_key_exists($key, $this->$refColumn)) {
			$array = $this->$refColumn;
			$key = $array[$key];
		}

		if (isset($this->_cache[$key])) {
			return $this->_cache[$key];
		}

		return null;
	}

	/**
	 * Storage row in the cache
	 *
	 * @param Db_Row $row
	 * @return void
	 */
	public function setCache($row, $refColumn = '')
	{
		$this->_cache[ $row->getId() ] = $row;
		if ($refColumn) {
			$value = $row->__get($refColumn);
			if (! isset($this->$refColumn)) {
				$this->$refColumn = array();
			}
			/*$arr = $this->$refColumn;
			if (!is_array($arr)) {
				$this->$refColumn = array();
			}*/
			$refColumnCache = $this->$refColumn;
			$refColumnCache[$value] = $row->getId();
			$this->$refColumn = $refColumnCache;
		}
	}

	public function doLoadDependent($rowset, $columnName = 'id')
	{

		$map = $this->_referenceMap;
		foreach ($map as $reference) {
			if ($reference['columns'] == $columnName) {
				$class = $reference['refTableClass'];
				$refColumn = $reference['refColumns'];
				break;
			}
		}

		$parentPeer = call_user_func(array($class, 'getInstance'));
		$colValues = array();
		foreach ($rowset as $row) {
			$val = $row->__get($ref_column);
			if (!in_array($val, $colValues)) {
				$colValues[] = $val;
			}
		}

		$select = $this->select();
		if (count($colValues) > 1) {
			$select->where("`$columnName` in (?)", $colValues);

		} else {
			$select->where("`$columnName` = ?", $colValues);
		}
		$rowset = $this->fetchAll($select);


//		foreach ($rowset as $row) {
//			$this->setCache($row, $columnName);
//		}
	}


	/**
	 * Loads parent/children chain start from $list.
	 *
	 * @param Zend_Db_Table_Rowset|array  $list object/array of BaseObjects.
	 * @param string|array $foreign Foreign key column quality name and criteria
	 *
	 * @return array of rows last loaded objects in chain.
	 */
	public static function doLoadDepsFor($list)
	{
		// Get list of passed arguments
		$args = func_get_args();
		// If there is no foreign key info or no list given return empty array
		if (count($args) < 2) {
			return array();
		}

		// Get List of objects to load its dependencies (first passed argument)
		$list = array_shift($args);
		//$list = is_array($list) ? $list : array($list);
		if (!count($list)) { return array(); }

		// Get foreign key information (second passed argument)
		$foreign  = array_shift($args);
		$column   = is_array($foreign) ? array_shift($foreign) : $foreign;
		$criteria = is_array($foreign) ? array_shift($foreign) : null;

		// Get passed list item table name
		//$itemTable = current($list)->getPeer()->getTableMap()->getPhpName();
		$itemTable = $list->getTable();
		// Get foreign column table info
		//$columnTable = self::_getTable($column);
		$map = $this->_referenceMap;
		foreach ($map as $reference) {
			if ($reference['columns'] == $columnName) {
				$columnTable = $reference['refTableClass'];
				$columnTable = call_user_func($columnTable, 'getInstance');
				//$refColumn = $reference['refColumns'];
				break;
			}
		}


		// Is column belongs to list item table?
		if ($itemTable == $columnTable['table']->getPhpName()) {
			// Is self-to-self reference?
			if ($columnTable['column']->getRelatedTableName() == $columnTable['table']->getName()) {
				$result = self::_doLoadChildrenFor($list, $column, $criteria);
			} else {
				$result = self::_doLoadParentFor($list, $column);
			}
		} else {
			$result = self::_doLoadChildrenFor($list, $column, $criteria);
		}

		// More deep load?
		if (count($args) > 0) {
			array_unshift($args, $result);
			$result = call_user_func_array(__METHOD__, $args);
		}

		// Return loaded list
		return $result;
	}

	/**
	 * Load children object for $parents.
	 *
	 * @param array  $children   Array of BaseObject.
	 * @param string $column     Full quality column name.
	 * @param Criteria $criteria Filter for childrens.
	 *
	 * @return array
	 */
	private static function _doLoadChildrenFor($parents, $column, Criteria $criteria = null)
	{
		$childParam = self::_getTable($column);
		$parentParam = self::_getTable($childParam['column']->getRelatedName());

		// Select parent ids
		$parentId = array();
		foreach ($parents as $parent) {
			if (method_exists($parent, $parentParam['get'])) {
				$v = call_user_func(array($parent, $parentParam['get']));
				if (!empty($v)) {
					$parentId[$v] = $v;
				}
			}
		}

		if (!count($parentId)) {
			return array();
		}

		// Select children
		$criteria = $criteria ? $criteria : new Criteria();
		$c = clone $criteria;
		$fqn = $childParam['column']->getFullyQualifiedName();
		$c->add($fqn, $fqn .' IN (' .implode(',', $parentId). ')', Criteria::CUSTOM);
		$children = call_user_func(array($childParam['peer'], 'doSelect'), $c);

		$ch = array();
		foreach ($children as $child) {
			$id = call_user_func(array($child, $childParam['get']));
			$ch[$id][] = $child;
		}

		// Set parents for children method name
		$set = 'set' . $childParam['table']->getPhpName() . 's';
		// Check getter method existence beacuse setMethod has catched by __call() and I don`t know how check it :(
		if (method_exists($parents[0], 'get' . $childParam['table']->getPhpName(). 'sRelatedBy' . $childParam['column']->getPhpName())) {
			$set .= 'RelatedBy' . $childParam['column']->getPhpName();
		}

		foreach($parents as $parent) {
			if (method_exists($parent, $parentParam['get'])) {
				$pk = call_user_func(array($parent, $parentParam['get']));
				call_user_func(array($parent, $set), array_key_exists($pk, $ch) ? $ch[$pk] : array(), clone $criteria);
			}
		}
		unset($ch);

		return $children;
	}

	/**
	 * Load parent object for $children.
	 *
	 * @param array  $children Array of BaseObject.
	 * @param string $column   Full quality column name.
	 *
	 * @return array
	 */
	private static function _doLoadParentFor(array $children, $column)
	{
		$childParam = self::_getTable($column);
		$parentParam = self::_getTable($childParam['column']->getRelatedName());

		$set = 'set' . $parentParam['table']->getPhpName();
		// check on get method exist beacuse setMethod catch in __call() and i don`t know how check them :(
		if (count($children) && method_exists($children[0], 'get' . $parentParam['table']->getPhpName() . 'RelatedBy' . $childParam['column']->getPhpName())) {
			$set .= 'RelatedBy' . $childParam['column']->getPhpName();
		}

		// select children parentID
		$parentId = array();
		$ch = array();
		foreach ($children as $child) {
			$id = call_user_func(array($child, 'get' . $childParam['column']->getPhpName()));
			if (!empty($id)) {
				$parentId[$id] = $id;
				$ch[$id][] = $child;
			}
		}
		$children = $ch;
		unset($ch);

		if (!count($parentId)) {
			return array();
		}

		$c = new Criteria();
		$fqn = $parentParam['column']->getFullyQualifiedName();
		$c->add($fqn, $fqn .' IN (' .implode(',', $parentId). ')', Criteria::CUSTOM);
		$parents = call_user_func(array($parentParam['peer'], 'doSelect'), $c);

		// set parents
		foreach ($parents as $parent) {
			$pk = call_user_func(array($parent, $parentParam['get']));
			foreach ($children[$pk] as $child) {
				call_user_func(array($child, $set), $parent);
			}
		}

		return $parents;
	}

	/**
	 * Return complex table information by full qualified column name
	 *
	 * @param string $column Full quality column name.
	 * @return array
	 */
	private static function _getTable($column)
	{
		$peer = implode('', array_map('ucfirst', explode('_', array_shift(explode('.', $column))))) . 'Peer';

		$tm = call_user_func(array($peer, 'getTableMap'));
		$cm = $tm->getColumn($column);

		return array(
			'peer'   => $peer,
			'table'  => $tm,
			'column' => $cm,
			'get'    => 'get' . $cm->getPhpName(),
		);
	}


	/**
	 * Return string with table name.
	 *
	 * @return string
	 */
	public function getTableName()
	{
		return $this->_name;
	}
	
	
	/**
	 * получить зависимые родительские объекты
	 * (очень просто но работает)
	 * 
	 * @param unknown_type $rowset
	 * @param unknown_type $foreignKey
	 */
	public function findParentRowset($rowset, $ruleKey, Zend_Db_Table_Select $select = null, $params = array()) {
		
		//@todo необходимо добавить всякие проверки 
		
		//проходим по всем записям собирая необходимые id
		$colValues = array();
		foreach ($rowset as $row) {
			$val = $row->__get($ruleKey);
			if (!empty($val) && !in_array($val, $colValues)) {
				$colValues[] = $val;
			}
		}
		
		//получаем зависимый класс
		$map = $this->_referenceMap;
		foreach ($map as $reference) {
			if ($reference['columns'] == $ruleKey) {
				$class = $reference['refTableClass'];
				$refColumn = $reference['refColumns'];
				break;
			}
		}

		if (empty($refColumn) || count($colValues) == 0
			|| empty($class) || !class_exists($class)) {
			return false;
		}
		
		//формируем запрос
		$peer = call_user_func($class.'::getInstance');
		if ($select === null) {
            $select = $peer->select();
        } else {
            $select->setTable($peer);
        }
		if (count($colValues) > 1) {
			$select->where('`'.$refColumn.'` IN (?)', $colValues);
		} else {
			$select->where('`'.$refColumn.'` = ?', $colValues);
		}
		
		if(isset($params['cache']['time'])) {
			$cacheTime = $params['cache']['time'];
		} else {
			$cacheTime = self::CACHE_TIME_LIMIT;
		}
		
		if(!isset($params['cache']['force'])) {
			$parentRowset = $this->_cacheSelect($select, $peer, $cacheTime);
		} else 
		{
			$parentRowset = $peer->fetchAll($select);
		}
		
		
		return $parentRowset;
	}
	
	/**
	 * Получить зависимые дочерние элементы
	 * (просто но работает)
	 * 
	 * @param unknown_type $rowset
	 * @param unknown_type $foreignKey
	 */
	public function findDependentRowset($rowset, $ruleKey, Zend_Db_Table_Select $select = null, $params = array()) {
		
		//@todo необходимо добавить всякие проверки
		
		//получаем зависимый класс
		$map = $this->_referenceMap;
		foreach ($map as $reference) {
			if ($reference['columns'] == $ruleKey) {
				//$class = $reference['refTableClass'];
				$refColumn = $reference['refColumns'];
				break;
			}
		}
		
		if (empty($refColumn)) {
			return false;
		}
			
		//проходим по всем записям собирая необходимые id
		$colValues = array();
		foreach ($rowset as $row) {
			$val = $row->__get($refColumn);
			if (!empty($val) && !in_array($val, $colValues)) {
				$colValues[] = $val;
			}
		}
		
		if (count($colValues) == 0) {
			return false;
		}
		
		//формируем запрос
		if ($select === null) {
            $select = $this->select();
        } else {
            $select->setTable($this);
        }
		if (count($colValues) > 1) {
			$select->where('`'.$ruleKey.'` IN (?)', $colValues);
		} else {
			$select->where('`'.$ruleKey.'` = ?', $colValues);
		}
		
		if(isset($params['cache']['time'])) {
			$cacheTime = $params['cache']['time'];
		} else {
			$cacheTime = self::CACHE_TIME_LIMIT;
		}
		
		if(!isset($params['cache']['force'])) {
			$dependentRowset = $this->_cacheSelect($select, $this, $cacheTime);
		} else {
			$dependentRowset = $this->fetchAll($select);
		}
		
		return $dependentRowset;
	}
	
	/**
	 * создание индексного массива по необходимому ключу
	 * 
	 * @param rowset $rowset
	 * @param string $preferKey
	 * 
	 * @return array
	 */
	public function normalizeByPreferKey($rowset, $preferKey = 'id', $multi = false) {
		
		$rowsetArray = array();
		if (!empty($rowset)) {
			foreach ($rowset as $row) {
				if (is_object($row)) {
					$key = $row->__get($preferKey);
				} else {
					$key = $row[$preferKey];
				}
				if (!empty($key)) {
					if ($multi) {
						$rowsetArray[$key][] = $row;
					} else {
						$rowsetArray[$key] = $row;
					}
				}
			}
		}
		
		return $rowsetArray;
	}
	
	/**
	 * метод возвращающий массив контент листов
	 * @param DB_Rowset $rowset
	 * @param array $params
	 * 
	 * @return array|boolean
	 */
	public function getContentList($rowset, $params = array()) {
		return false;
	}
	
	/**
	 * работа с кешем запроса
	 * 
	 * @param object $select
	 * @param object $peer
	 * @param integer $cacheTime
	 * @return mixed
	 */
	public function _cacheSelect($select, $peer, $cacheTime = 3600) {
		
		$except = false;
		$cacheslot = new Cache_Slot_Query($select, $cacheTime);
		$data = $cacheslot->load();
		if ($data) {
			//при неудаче работы с объектом из кеша. осуществляем запрос
			//при постоянных ошибках кеш бесполезен :(
			try {
				$contentRowset = $data;
				$contentRowset->setTable($peer);
			} catch (Exception $e) {
				//@todo хотя метод setTable и кидает исключение иногда, но установку имени талицы он делает.
				//поэтому если $except = false; все работает и даже через кеш (но могут быть проблемы в дальнейшей работе с объектом)
				$except = true;
			}
		}
		
		if ($except || !$data) {
			$contentRowset = $peer->fetchAll($select);
			$cacheslot->save($contentRowset);
		}
		
		return $contentRowset;
	}
	
	public function _cacheSelectRow($select, $peer, $cacheTime = 3600) {
		
		$except = false;
		$cacheslot = new Cache_Slot_Query($select, $cacheTime);
		$data = $cacheslot->load();
		if ($data) {
			try {
				$row = $data;
				$row->setTable($peer);
			} catch (Exception $e) {
				$except = true;
			}
		}
		
		if ($except || !$data) {
			$row = $peer->fetchRow($select);
			$cacheslot->save($row);
		}
		
		return $row;
	}
}
